(ns metabase.test.data.datasets
  "Interface + implementations for loading test datasets for different drivers, and getting information about the
  dataset's tables, fields, etc.

  TODO - rename this to `metabase.driver.test-extensions.expect` or something like that"
  (:require [expectations :refer [expect]]
            [metabase.driver :as driver]
            [metabase.test.data.env :as tx.env]))

;; # Helper Macros

(defn do-when-testing-driver
  "Call function `f` (always with no arguments) *only* if we are currently testing against `driver`.
   (This does NOT bind `*driver*`; use `driver/with-driver` if you want to do that.)"
  {:style/indent 1}
  [driver f]
  (when (contains? tx.env/test-drivers driver)
    (f)))

(defmacro when-testing-driver
  "Execute `body` only if we're currently testing against `driver`.
   (This does NOT bind `*driver*`; use `with-driver-when-testing` if you want to do that.)"
  {:style/indent 1}
  [driver & body]
  `(do-when-testing-driver ~driver (fn [] ~@body)))

(defmacro with-driver-when-testing
  "When `driver` is specified in `DRIVERS` env var, binds `metabase.driver/*driver*` and executes `body`. The currently
  bound driver is used for calls like `(data/db)` and `(data/id)`."
  {:style/indent 1}
  [driver & body]
  `(let [driver# ~driver]
     (when-testing-driver driver#
       (driver/with-driver driver#
         ~@body))))

(defmacro expect-with-driver
  "Generate a unit test that only runs if we're currently testing against `driver`, and that binds `*driver*` when it
  runs."
  {:style/indent 1}
  [driver expected actual]
  `(when-testing-driver ~driver
     (expect
       (driver/with-driver ~driver ~expected)
       (driver/with-driver ~driver ~actual))))

(defn- doexpect-with-driver
  "Internal impl for `expect-with-drivers`. Similar to expectations' `doexpect` macro, but tweaked to provide better
  output for the tests generated by `expect-with-drivers`."
  [driver get-e get-a e-form a-form]
  (with-driver-when-testing driver
    (let [e-form (list 'metabase.driver/with-driver driver e-form)
          a-form (list 'metabase.driver/with-driver driver a-form)
          e      (try (get-e) (catch Throwable t t))
          a      (try (get-a) (catch Throwable t t))]
      (expectations/report
       (try
         (expectations/compare-expr e a e-form a-form)
         (catch Throwable t
           (expectations/compare-expr t a e-form a-form)))))))

(defn doexpect-with-drivers [drivers get-e get-a e-form a-form]
  (doseq [driver drivers]
    (doexpect-with-driver driver get-e get-a e-form a-form)))

(defn get-drivers-or-fail [f]
  (try
    (f)
    (catch Throwable e
      (expectations/report {:type :fail, :message (.getMessage e)})
      nil)))

(defmacro expect-with-drivers
  "Generate unit tests for all drivers in env var `DRIVERS`; each test will only run if we're currently testing the
  corresponding driver. `*driver*` is bound to the current driver inside each test."
  {:style/indent 1}
  [drivers expected actual]
  ;; Make functions to get expected/actual so the code is only compiled one time instead of for every single driver
  ;; speeds up loading of metabase.driver.query-processor-test significantly
  `(let [get-e# (fn [] ~expected)
         get-a# (fn [] ~actual)]
     (defn ~(vary-meta (symbol (str "expect-with-drivers-" (hash &form)))
                       assoc :expectation true)
       []
       (doexpect-with-drivers (get-drivers-or-fail (^:once fn* [] ~drivers)) get-e# get-a# '~expected '~actual))))

(defmacro expect-with-all-drivers
  "Generate unit tests for all drivers specified in env var `DRIVERS`. `*driver*` is bound to the current driver inside
  each test."
  {:style/indent 0}
  [expected actual]
  `(expect-with-drivers tx.env/test-drivers ~expected ~actual))
